現今大多數的軟體工程都是以網路工程為主,那網路工程中又以 Web API 為單位做為開發的基石;因此,今天我們了解如何撰寫 Web API 的單元測試,來提升軟體開發的準確性。那首先,我們要介紹 Web API (假設部分在閱覽的讀者沒有接觸網路工程),Web API —— Web Application Programming Interface,白話文就是網路應用程式與網路應用程式之間溝通的橋樑,那 Web API 可依據使用者開發決定提供 XML、Json、GeoJson、File...等,以下幾篇為我覺得不錯的 Web API 參考文章,供各位參考:
那通常檢測 Web API,都是在檢測相對應的 Controller(以瀏覽器來看是一個對應的 Url),因應不同的 Controller 會有不同的服務如 HttpGet、HttpPost...等,那這次的範例是以 HttpPost 為例。
於是乎,我們就開始撰寫 Web API 專案,那這次範例所採用的框架是 N-Tiers 框架,主要流程是
Controller -> IService (實作用 Service) -> Model
而今天的情境是有使用者登錄商品的資料,登錄完資料後,我們要登記 Log 並且給予這個資料一組 Unique GUID,程式碼如下:
Controller 層:
namespace Products.Controllers
{
[Route("api/[controller]")]
public class ProductsController : Controller
{
private readonly ILogger Logger;
private readonly IProductService ProductService;
public ProductsController(ILogger inLogger, IProductService inProductService)
{
Logger = inLogger;
ProductService = inProductService;
}
[HttpPost]
public string Post(Product product)
{
Logger.Log("High", $"Adding a products with an id {product.ProductName}");
var productGuid = ProductService.SaveProduct(product);
return productGuid;
}
}
}
IService 層(服務介面層):
namespace Products.IService
{
// 登記 Log 的服務
public interface ILogger
{
public void Log(string level, string message);
}
// 產品 Product 的服務
public interface IProductService
{
public string SaveProduct(Product product);
}
}
Service 層(服務實作層):
namespace Products.Service
{
// 登記 Log 的服務
public class Logger : ILogger
{
public void Log(string level, string message)
{
// 撰寫你要的功能,並非測試重點,所以不詳細列述
}
}
// 產品 Product 的服務
public class ProductService : IProductService
{
public string SaveProduct(Product product)
{
// 撰寫你要的功能,並非測試重點,所以不詳細列述
}
}
}
Model 層(資料模型):
namespace Products.Models
{
public class Product
{
// 產品序號
public string ProductId;
// 產品名稱
public string ProductName;
// 產品現貨數量
public int QuantityAvailable;
}
}
所以,我們要檢測 HttpPost 是否正常運行,可思考幾個關注點:
列好了關注點之後,就可以開始撰寫測試,如下:
[TestFixture]
public class ProductsControllerTests
{
private ILogger Logger;
private IProductService ProductService;
private ProductsController ProductsController;
[SetUp]
public void SetUp()
{
Logger = Substitute.For<ILogger>();
ProductService = Substitute.For<IProductService>();
ProductsController = new ProductsController(Logger, ProductService);
}
[Test]
public void DemoGuidTest()
{
// Arrange
var guid = "af95003e-b31c-4904-bfe8-c315c1d2b805";
var product = new Product { ProductId = "1", ProductName = "Oven", QuantityAvailable = 3 };
Logger.Log(default, default);
ProductService.SaveProduct(product).Returns(guid);
// Act
var result = ProductsController.Post(product);
// Assert
Assert.AreEqual(result, guid);
}
[Test]
public void DemoSaveProductReceiveTest()
{
// Arrange
var guid = "af95003e-b31c-4904-bfe8-c315c1d2b805";
var product = new Product { ProductId = "1", ProductName = "Oven", QuantityAvailable = 3 };
Logger.Log(default, default);
ProductService.SaveProduct(product).Returns(guid);
// Act
var result = ProductsController.Post(product);
// Assert
ProductService.Received(1).SaveProduct(product);
}
[Test]
public void DemoLogReceiveTest()
{
// Arrange
var guid = "af95003e-b31c-4904-bfe8-c315c1d2b805";
var product = new Product { ProductId = "1", ProductName = "Oven", QuantityAvailable = 3 };
Logger.Log(default, default);
ProductService.SaveProduct(product).Returns(guid);
// Act
var result = ProductsController.Post(product);
// Assert
Logger.Received(1).Log(default, default);
}
}
若實務上,這三個測試都成功,則代表的確有呼叫到 Log 與 SaveProduct 各一次,並且有成功回傳 GUID,這樣就算是個良好的單元測試組。
文章故事情境參考來源:https://www.michalbialecki.com/2019/01/03/writing-unit-tests-with-nunit-and-nsubstitute/